
import java.io.*;
import java.util.concurrent.*;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.*;

/**
 * This class uses a shared buffer to allow copying a source
 * file to a destination. Two threads Reader and Writer are
 * controlled by a lock to access the shared buffer.
 * 
 * @author Chen Wei
 */
public class FileCopy {

    public static void main(String[] args) {
        if (args.length < 2) {
            System.err.println("Usage: java FileCopy src dst");

        }
        else {
            FileCopy fc = new FileCopy(args[0], args[1]);
            fc.copy();
        }
    }
    private String src, dst;
    //
    // 
    //
    private static Lock lock = new ReentrantLock();
    private static Condition newReader = lock.newCondition();
    private static byte[] sharedBuffer = null;
    /* sharednBytes indicates the actual valid bytes to be
     * shared in the sharedBuffer. */
    private static int sharednBytes = 0;
    /* bytesWritten indicates the total bytes already written
     * to the $dst file by Writer. */
    private static long bytesWritten = 0;
    private static long totalBytes = 0; // size of $src file

    public FileCopy(String src, String dst) {
        this.src = src;
        this.dst = dst;
    }

    /**
     * This function news a thread pool and executes two tasks
     * to perform the copy action and waits for all the tasks
     * to finish.
     */
    public void copy() {
        ExecutorService executor = Executors.newCachedThreadPool();
        executor.execute(new Writer());
        executor.execute(new Reader());
        executor.shutdown();

        while (!executor.isTerminated()) {
        }
        System.out.println("completed!");
    }

    /**
     * This inner class is a task to read 1024 bytes from $src 
     * file to $sharedBuffer under the lock concurrency control.
     * And it only puts the content to the $sharedBuffer when
     * the buffer is null; otherwise, it will release the lock 
     * and wait for Writer to get the content from the $sharedBuffer.
     */
    class Reader implements Runnable {

        public void run() {
            try {
                BufferedInputStream in = new BufferedInputStream(new FileInputStream(src));
                totalBytes = in.available();

                int nbytes = 0;
                do {
                    byte[] buffer = new byte[1024];
                    nbytes = in.read(buffer, 0, buffer.length);

                    //
                    // Critical section
                    //
                    lock.lock();
                    try {
                        if (nbytes != -1) {
                            if (sharedBuffer != null) {
                                newReader.await();
                            }

                            sharedBuffer = buffer;
                            sharednBytes = nbytes;
                        }
                    }
                    catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                    finally {
                        lock.unlock();
                    }
                } while (nbytes != -1);
                in.close();

            }
            catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    
    /**
     * This inner class is a task to write $sharednBytes bytes
     * from $sharedBuffer under the lock concurrency control 
     * to $dst file.
     * And it only gets the content from the $sharedBuffer when
     * the buffer is not null. After getting the bytes in the
     * $sharedBuffer, it will notify Reader to awake.
     * A $finished boolean variable is set when the bytes written
     * to $dst file equals the size of $src file, hence finishing
     * the task.
     */    
    class Writer implements Runnable {

        public void run() {
            try {
                BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(dst));
                boolean finished = false;

                while (!finished) {
                    //
                    // Critical section
                    //
                    // out.write(...);
                    //
                    lock.lock();
                    try {
                        if (sharedBuffer != null) {
                            out.write(sharedBuffer, 0, sharednBytes);
                            bytesWritten += sharednBytes;
                            sharedBuffer = null;
                            newReader.signalAll();
                        }

                        if (bytesWritten == totalBytes && totalBytes != 0) {
                            finished = true;
                        }
                    }
                    finally {
                        lock.unlock();
                    }
                }
                out.close();

            }
            catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
